﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace BacteriaSimulation.Simulation
{
    public delegate void SimulationUpdateEventHandler(SimulationUpdateEventArgs args);

    public enum SimulationState
    {
        RUNNING, PAUSED, STOPPED
    }

    public enum WALL_COLLISION_TYPE
    {
        RIGHT, TOP, BOTTOM, LEFT
    }

    public class Simulation
    {
        /// <summary>
        /// Simulation period in seconds
        /// </summary>
        public Double SimulationPeriodSec { get; set; }

        public static readonly int DEFAULT_GRID_ROW_COUNT = 8;
        public static readonly int DEFAULT_GRID_COLUMN_COUNT = 16;

        protected Random random = new Random();
        public Random Random
        {
            get
            { return this.random; }
        }

        public Queue<SimulationCommand> CommandQueue { get; set; }
        public SimulationState State {get; set; }
        public int SimulationStepInterval { get; set; }
        public SimulationEnvironment Env { get; set; }

        public List<Bacterium> RemovingBacteria { get; set; }
        public List<Bacterium> AddingBacteria { get; set; }

        protected int cyanobacteriaCount = 0;
        protected int cannibalBacteriaCount = 0;

        protected IndexGrid grid;
        public IndexGrid Grid
        {
            get
            {
                return this.grid;
            }

            set 
            {
                this.grid = value;
            }
        }

        protected List<Bacterium> Bacteria
        {
            get;
            set;
        }

        public event SimulationUpdateEventHandler SimulationUpdate;

        public Object UpdateSyncObject { get; set; }

        public TaskChain TaskChain { get; set; }

        public Simulation(SimulationEnvironment environment) :
            this(environment, DEFAULT_GRID_ROW_COUNT, DEFAULT_GRID_COLUMN_COUNT)
        {
        }

        public Simulation(SimulationEnvironment environment, int gridRowCount, int gridColumnCount)
        {
            this.SimulationPeriodSec = 0.04;
            this.CommandQueue = new Queue<SimulationCommand>();
            this.State = SimulationState.STOPPED;
            SimulationStepInterval = environment.SimulationStepInterval;
            this.Env = environment;
            this.Bacteria = new List<Bacterium>();
            RemovingBacteria = new List<Bacterium>();
            AddingBacteria = new List<Bacterium>();
            this.Grid = new IndexGrid(Env, gridRowCount, gridColumnCount);
            UpdateSyncObject = new Object();

            this.TaskChain = TaskChain.BuildDefaultChain(this, Env);
        }

        public void SimulationThreadStart()
        {
            try
            {
                this.State = SimulationState.RUNNING;
                Loop();
                System.Diagnostics.Debug.WriteLine("Exiting simulation thread");
            }
            catch(Exception e)
            {
                System.Console.WriteLine(e.Source + Environment.NewLine + e.StackTrace);
            }
        }

        public void Loop()
        {
            while(State != SimulationState.STOPPED)
            {
                SimulationCommand command = DequeueCommand();
                if(command != null)
                {
                    ExecuteCommand(command);
                }

                if(State == SimulationState.RUNNING)
                {
                    PerformSimulationStep();
                    OnSimulationUpdate();
                    //Thread.Sleep(SimulationStepInterval);
                }
                else if (State == SimulationState.PAUSED)
                {
                    lock (CommandQueue)
                    {
                        Monitor.Wait(CommandQueue);
                    }
                    continue;
                }
                else if (State == SimulationState.STOPPED)
                {
                    break;
                }
            }
        }

        protected void PerformSimulationStep()
        {            
            // Update the list of bacteria, removing the dead bacteria and adding the new ones
            lock (this.Bacteria)
            {
                foreach (Bacterium removing in RemovingBacteria)
                {
                    RemoveBacterium(removing);
                    removing.NotifyDeath();
                }
                foreach (Bacterium adding in AddingBacteria)
                {
                    AddBacterium(adding);
                }
            }
            this.RemovingBacteria.Clear();
            this.AddingBacteria.Clear();

            foreach (Bacterium bact in Bacteria)
            {
                bact.PerformSimulationStep(Env);
                //UpdateBacteriumPosition(bact);
                ExecuteSimulationTasks(bact);
            }

        }

        protected void ExecuteSimulationTasks(Bacterium bacterium)
        {
            TaskChain.execute(bacterium);
        }

        protected void UpdateBacteriumPosition(Bacterium bact)
        {
            if (bact.State == BacteriumState.DEAD)
            {
                this.RemovingBacteria.Add(bact);
                return;
            }

            ITask checkDuplicateTaks = new DuplicateBacteriumTask();
            checkDuplicateTaks.executeTask(this, bact, Env);

            ITask task = new UpdateBacteriumPositionTask();
            task.executeTask(this, bact, Env);

            task = new ApplyForceFieldTask();
            task.executeTask(this, bact, Env);

            // Detect possible wall collision
            if (bact.X > (Env.Width - bact.Width))
            {
                bact.X = Env.Width - bact.Width - Env.Epsilon;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.RIGHT, Env);
            }
            if (bact.X < 0)
            {
                bact.X = 0 + Env.Epsilon;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.LEFT, Env);
            }
            if (bact.Y < 0)
            {
                bact.Y = 0 + Env.Epsilon;
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.TOP, Env);
            }
            if (bact.Y > (Env.Height - bact.Height))
            {
                bact.Y = (Env.Height - bact.Height - Env.Epsilon);
                bact.WallCollisionDetected(WALL_COLLISION_TYPE.BOTTOM, Env);
            }

            // detect possible collision with other bacteria
            if (Env.DetectBacteriaCollision)
            {
                // chek the cell to which the bacterium belongs
                foreach (Bacterium other in bact.Cell.Bacteria)
                {
                    if (DetectCollision(bact, other))
                    {
                        HandleBacteriaOverlap(bact, other);
                        bact.BacteriumCollisionDetected(other, Env);
                        //other.BacteriumCollisionDetected(bact, Env);
                    }
                }

                // check the adjacent cells
                foreach (IndexGridCell cell in Grid.GetAdjacentCells(bact.Cell.RowIndex, bact.Cell.ColumnIndex))
                {
                    foreach(Bacterium other in cell.Bacteria)
                    {
                        if(DetectCollision(bact, other))
                        {
                            HandleBacteriaOverlap(bact, other);
                            bact.BacteriumCollisionDetected(other, Env);
                            //other.BacteriumCollisionDetected(bact, Env);
                        }
                    }
                }

                bact.AdjustPosition(Env);

                this.Grid.Assign(bact);
            }
        }

        protected void HandleBacteriaOverlap(Bacterium bacterium, Bacterium other)
        {
            Double radius1 = bacterium.Width / 2;
            Double radius2 = other.Width / 2;
            // computes the coordinates of the centers of the bacteria circles:
            Double x1 = bacterium.X + radius1;
            Double y1 = bacterium.Y + radius1;
            Double x2 = other.X + radius2;
            Double y2 = other.Y + radius2;
            Double distance = SimulationPoint.Distance(new SimulationPoint(x1, y1), new SimulationPoint(x2, y2));

            Double halfOverlap = ((radius1 + radius1) - distance) / 2;
            double dx1 = (x1 - x2) / distance;
            double dy1 = (y1 - y2) / distance;
            double dx2 = (x2 - x1) / distance;
            double dy2 = (y2 - y1) / distance;

            // new coordinates of the bacteria
            double bact1X = x1 + (halfOverlap * dx1) - radius1;
            double bact1Y = y1 + (halfOverlap * dy1) - radius1;
            double bact2X= x2 + (halfOverlap * dx2) - radius2;
            double bact2Y= y2 + (halfOverlap * dy2) - radius2;
            Boolean overflow = false;

            if (bact1X < 0 || bact1X > Env.Width - radius1 * 2)
            {
                // cannot move bacterium over the edges of the environment
                bact1X = x1;
                //bact2X = x2 + (halfOverlap * 2 * dx2) - radius2;
                overflow = true;
            }
            if (bact1Y < 0 || bact1Y > Env.Height - radius1 * 2)
            {
                bact1Y = y1;
                //bact2Y = y2 + (halfOverlap * 2 * dy2) - radius2;
                overflow = true;
            }
            if (bact2X < 0 || bact2X > Env.Width - radius2 * 2)
            {
                // cannot move bacterium over the edges of the environment
                bact2X = x2;
                //bact1X = x1 + (halfOverlap * 2 * dx1) - radius1;
                overflow = true;
            }
            if (bact2Y < 0 || bact2Y > Env.Height - radius2 * 2)
            {
                bact2Y = y2;
                //bact1Y = y1 + (halfOverlap * 2 * dy1) - radius1;
                overflow = true;
            }

            bacterium.X = bact1X;
            bacterium.Y = bact1Y;
            other.X = bact2X;
            other.Y = bact2Y;
            //System.Diagnostics.Debug.WriteLineIf(overflow, "bactX1 = " + bact1X + ", bactY1 = " + bact1Y + ", bactX2 = " + bact2X + ", bactY2 = " + bact2Y);
            //System.Diagnostics.Debug.WriteLineIf(overflow, "X1 = " + x1 + ", Y1 = " + y1 + ", X2 = " + x2 + ", Y2 = " + y2);
        }

        public static Boolean DetectCollision(Bacterium bacterium, Bacterium other)
        {
            if (other == bacterium)
            {
                return false;
            }
            SimulationPoint center = new SimulationPoint(bacterium.Position.X + bacterium.Width / 2, bacterium.Position.Y + bacterium.Height / 2);
            SimulationPoint otherCenter = new SimulationPoint(other.Position.X + other.Width / 2, other.Position.Y + other.Height / 2);
            double bacteriumRadius = bacterium.Width / 2;
            double otherBacteriumRadius = other.Width / 2;

            double dx = otherCenter.X - center.X;
            double dy = otherCenter.Y - center.Y;
            bool collision = (dx * dx) + (dy * dy) <= Math.Pow((bacteriumRadius + otherBacteriumRadius), 2);

            return collision;
        }

        protected void ExecuteCommand(SimulationCommand command)
        {
            switch (command.CommandName)
            {
                case "pause":
                    {
                        this.State = SimulationState.PAUSED;
                        break;
                    }
                case "resume":
                    {
                        this.State = SimulationState.RUNNING;
                        break;
                    }
                case "stop":
                    {
                        this.State = SimulationState.STOPPED;
                        break;
                    }
                case "add":
                    {
                        AddBacterium(command as AddCommand);
                        break;
                    }
                case "remove":
                    {
                        RemoveBacterium(command as RemoveCommand);
                        break;
                    }
                case "killRange":
                    {
                        KillRange(command as KillRangeCommand);
                        break;
                    }
                case "dumpIndex":
                    {
                        DumpIndex();
                        break;
                    }
            }
        }

        protected void KillRange(KillRangeCommand cmd)
        {
            if (cmd != null)
            {
                foreach (Bacterium bact in Bacteria)
                {
                    if (bact.X >= cmd.X && bact.X <= cmd.X + cmd.Width
                        && bact.Y >= cmd.Y && bact.Y <= cmd.Y + cmd.Height)
                    {
                        bact.State = BacteriumState.DEAD;
                        Env.AvalaibleEnergyResource += bact.Energy;
                        RemovingBacteria.Add(bact);
                    }
                }
            }
        }

        protected void RemoveBacterium(RemoveCommand removeCmd)
        {
            if (removeCmd != null)
            {
                RemoveBacterium(removeCmd.Bacterium);
            }
        }

        protected void AddBacterium(AddCommand addCommand)
        {
            if (addCommand != null)
            {
                AddBacterium(addCommand.Bacterium);
            }
        }

        protected void AddBacterium(Bacterium adding)
        {
            adding.AdjustPosition(Env);
            Bacteria.Add(adding);
            if (adding.Diet == DietType.HERBIVORE)
            {
                cyanobacteriaCount += 1;
            }
            else if (adding.Diet == DietType.CARNIVORE)
            {
                cannibalBacteriaCount += 1;
            }
            this.Grid.Assign(adding);
        }

        protected void RemoveBacterium(Bacterium removing)
        {
            this.Bacteria.Remove(removing);
            if (removing.Diet == DietType.HERBIVORE)
            {
                cyanobacteriaCount -= 1;
            }
            else if (removing.Diet == DietType.CARNIVORE)
            {
                cannibalBacteriaCount -= 1;
            }
            this.Grid.Remove(removing);
        }

        protected void DumpIndex()
        {
            int total = 0;
            foreach (IndexGridCell cell in Grid.Cells)
            {
                System.Diagnostics.Debug.WriteLine("cell at [" + cell.RowIndex + ", " + cell.ColumnIndex + "] : " + cell.Bacteria.Count);
                total += cell.Bacteria.Count;
            }

            System.Diagnostics.Debug.WriteLine("Total: " + total);
        }

        protected SimulationCommand DequeueCommand()
        {
            try
            {
                bool lockAcquired = System.Threading.Monitor.TryEnter(CommandQueue);
                if (lockAcquired && CommandQueue.Count > 0)
                {
                    return CommandQueue.Dequeue();
                }
                else
                {
                    return null;
                }
            }
            finally
            {
                Monitor.Exit(CommandQueue);
            }
        }

        public void EnqueueCommand(SimulationCommand command)
        {
            try
            {
                bool lockAcquired = Monitor.TryEnter(CommandQueue);
                if (lockAcquired)
                {
                    CommandQueue.Enqueue(command);
                    Monitor.PulseAll(CommandQueue);
                }
            }
            finally
            {
                Monitor.Exit(CommandQueue);
                //Monitor.PulseAll(CommandQueue);
            }
        }

        protected void OnSimulationUpdate()
        {
            if(SimulationUpdate != null)
            {
                SimulationUpdateEventArgs args = new SimulationUpdateEventArgs(this.Bacteria, this.cyanobacteriaCount, this.cannibalBacteriaCount,
                    Env.AvalaibleEnergyResource);
                SimulationUpdate(args);
                //this.State = SimulationState.PAUSED;
            }
        }
    }
}
